#include "Engine.h"

Engine Engine::eInstance; // Our engine instance, made available via static

// Constructor. Automatically called with use of the singleton engine instance
Engine::Engine()
{
    // Defaults
    screenWidth = 1024;
    screenHeight = 768;
    screenDepth = 32;
    title = "TLA Engine";
    isActive = true;
    isRunning = false;
    isInit = false;
    revisionNumber = "-1";
    // FPS
    fps = 0;
    fpsUpdateRate = 1000; // 1 sec update rate
    // SDL related setup
    sdlFlags = SDL_OPENGL;
    // Viewing/Camera
    // Note: Cameras should not be drawed, but should passed events and updates
    currentCamera = 0; // Screens responsible for creating their own cameras

    #if defined(_DEBUG)
    std::cout << "Engine constructed." << std::endl;
    #endif
}

// Returns the singleton instance of the engine
Engine& Engine::Instance()
{
    return eInstance;
}

// Initialize display window/screen and graphics for the first time
bool Engine::init()
{
    if (isRunning)
    {
        std::cout << "Initializing stopped: Engine is already running." << std::endl;
        return false;
    }

    // Initialize SDL
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
    {
        std::cout << "Unable to initialize SDL: " << SDL_GetError() << std::endl;
        return false;
    }

    // Windows cmd fix due to SDL
    // See: http://www.libsdl.org/cgi/docwiki.cgi/FAQ_Console
    #if defined(_DEBUG) && (defined(__WIN32) || defined(__MINGW32__))
    freopen("CON", "w", stdout);
    freopen("CON", "w", stderr);
    #endif

    // Set SDL window title
    SDL_WM_SetCaption(title.c_str(), NULL);

    // Adjust bbp (screen depth) according to SDL video information
    const SDL_VideoInfo *videoInfo = SDL_GetVideoInfo();
    if(!videoInfo)
        std::cout << "SDL video query failed." << std::endl;
    else
        screenDepth = videoInfo->vfmt->BitsPerPixel;

    // Hide mouse
    //SDL_ShowCursor(SDL_DISABLE); // Temporarily disabled.

    // Initialize content manager
    if(!ContentManager::Initialize())
        return false;

    // Enable OpenGL double buffering. (SDL_GL_DOUBLEBUTTER is not to be used as an SDL video flag)
    // (Note: SDL_GL_SetAttribute intensionally called before creation of the SDL display surface)
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    //SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, vsync); // Set this attribute if we wanted to set up vsync

    // Create window (the SDL display surface)
    if(!loadWindow())
        return false;

    // Initialize OpenGL
    if(!initGL())
        return false;

    // Generate a listing of available screen modes
    enumScreenModes();

    // Screens (built-in engine screens)
    /*
    DebugConsole* debugCons = new DebugConsole();
    debugCons.visible = debugCons.eventable = debugCons.updatable = false;
    addScreen2D(debugCons);
    DebugHud* debugHud = new DebugHud;
    debugHud.visible = debugHud.eventable = debugHud.updatable = false;
    addScreen2D(debugHud);
    */

    MercurialRevision rev("revision");
    revisionNumber = rev.get();
    log("Revision number: " + revisionNumber);

    isInit = true;
    #if defined(_DEBUG)
    std::cout << "Engine initialized." << std::endl;
    #endif
    return true;
}

// Initialize OpenGL
bool Engine::initGL()
{
    /// Start GL code test
    // We want to enable alpha testing for transparent textures
    glEnable(GL_ALPHA_TEST);
    // Also blending for transparent textures
    glEnable(GL_BLEND);
    // Blend using the texture's alpha
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    // The texture is only visible when the resulting alpha is greater than 0.0f
    glAlphaFunc(GL_GREATER, 0.009f);
    /// End test

    glEnable(GL_TEXTURE_2D); // Enable 2d texture mapping
    glShadeModel(GL_SMOOTH); // Smooth (Gouraud) shading
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Set rgba clear color of the color buffer
    glClearDepth(1.0f); // Value for clearing the depth buffer
    glEnable(GL_DEPTH_TEST); // Enable depth testing
    glDepthFunc(GL_LEQUAL); // Define type of depth testing
    // http://www.opengl.org/sdk/docs/man/xhtml/glHint.xml
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Setting for perspective calculations

	// Check for errors
	if(glGetError() != GL_NO_ERROR)
	{
	    std::cout << "An OpenGL error occurred when initializing OpenGL." << std::endl;
		return false;
	}

    // Setup OpenGL for our window
    if(!resizeGL())
        return false;

    // Print GL information
    std::cout << "GL info:" << std::endl;
    std::cout << "\tvendor: " << glGetString(GL_VENDOR) << std::endl;
    std::cout << "\trenderer: " << glGetString(GL_RENDERER) << std::endl;
    std::cout << "\tversion: " << glGetString(GL_VERSION) << std::endl;
    std::cout << "\tshader version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl;
    //std::cout << "\textensions: " << glGetString(GL_EXTENSIONS) << std::endl;

    //SDL_WM_GrabInput(SDL_GRAB_ON);

    return true;
}

// Setup OpenGL to fit our window dimensions.
// Should be re-rerun when changing the screen dimensions.
bool Engine::resizeGL()
{
    // Setup screen viewport
    glViewport(0, 0, screenWidth, screenHeight);
    //glScissor(0, 0, screenWidth, screenHeight);

	// Calculate the aspect ratio of the window
	aspectRatio = (GLfloat)screenWidth / (GLfloat)screenHeight; // Aspect ratio of window

	// Check for errors
	if(glGetError() != GL_NO_ERROR)
	{
	    std::cout << "An OpenGL error occurred when resizing OpenGL." << std::endl;
		return false;
	}

    return true;
}

// Start and run the engine; begins the primary loop
int Engine::run()
{
    if (!isInit)
        return 0;

    isRunning = true;
    std::cout << "Engine running." << std::endl;
    loop();
    kill();
    return 0; // Clean run
}

// Main application loop
void Engine::loop()
{
    SDL_Event* event = new SDL_Event(); // Create our SDL event which will be passed through the scenegraph
    Uint32 deltaTime = 0; // in ms
    Uint32 frameTime = 0; // Time of current frame

    int frames = 0;
    Uint32 beginCounter = SDL_GetTicks();
    Uint32 deltaCounter = 0;
    while(isRunning)
    {
        // Update times
        deltaTime = SDL_GetTicks() - frameTime;
        frameTime = SDL_GetTicks();
        //float dt = deltaTime/1000.0f;

        // Calculate and display FPS
        frames++;
        deltaCounter = frameTime - beginCounter;
        if (deltaCounter >= fpsUpdateRate)
        {
            fps = frames / (deltaCounter / 1000);
            std::cout << "FPS: " << fps << std::endl;
            beginCounter = frameTime;
            frames = 0;
        }

        // Enter engine methods
        handleEvents(event);
        if(isActive)
            update(&deltaTime); // Only render when the window is active
        draw();
    }
    isRunning = false; // Ensure this is really false in case while loop was terminated unconventionally
}

// Draw 2D and 3D passes
// 3D is drawn first so that anything 2D shows on top
void Engine::draw()
{
    // Clear color buffer to the rgba color set by glClearColor()
    // And clear the depth buffer to the depth value set by glClearDepth()
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // OpenGL 3D Pass
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(45.0f, aspectRatio, 0.1f, 100.0f);   // TODO: should increase fov for widescreens
                                                        // http://www.codinghorror.com/blog/2007/08/widescreen-and-fov.html
	glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    // Manipulate world coordinates with camera
    if (currentCamera)
        currentCamera->setup();

    screenManager3D.drawAll();

    // OpenGL 2D Pass
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, screenWidth, screenHeight, 0, -1.0f, 1.0f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    screenManager2D.drawAll();

    // Update display (double buffering)
    SDL_GL_SwapBuffers(); // No need for glFlush since this is taken care for us here
}

void Engine::update(Uint32* deltaTime)
{
    screenManager3D.updateAll(deltaTime);
    screenManager2D.updateAll(deltaTime);
}

// Handle events
void Engine::handleEvents(SDL_Event* event)
{
    // Poll pending SDL input events
    while(SDL_PollEvent(event))
    {
        if(event->type == SDL_QUIT)
            isRunning = false;
        else if(event->type == SDL_ACTIVEEVENT)
        {
            // Check if window is focused. This allows for auto pausing when inactive
            if (event->active.state & SDL_APPINPUTFOCUS)
            {
                if(event->active.gain == 0)
                    isActive = false;
                else
                    isActive = true;
            }
        }

        screenManager3D.handleEventsAll(event);
        screenManager2D.handleEventsAll(event);
    }
}

// Quit initialized components
// Ideally one should be able to run init() a second time without problem after using kill()
void Engine::kill()
{
    isInit = false;
    std::cout << "Engine killed." << std::endl;
    SDL_Quit();
    // (Note: SDL_Quit() automatically frees the display surface created from SDL_SetVideoMode)
}

// Create window (the SDL display surface)
bool Engine::loadWindow()
{
    // Note: Use SDL_GetVideoSurface() to get the video display surface later
    if((SDL_SetVideoMode(screenWidth, screenHeight, screenDepth, sdlFlags)) == NULL)
    {
        std::cout << "Unable to create SDL display surface: " << SDL_GetError() << std::endl;
        return false;
    }

    return true;
}

void Engine::setTitle(std::string newTitle)
{
    title = newTitle;
    SDL_WM_SetCaption(title.c_str(), NULL); // Modify SDL window title
}

void Engine::addScreen2D(Screen* screen, int priority)
{
    screenManager2D.setPrimary(screen, priority);
}

void Engine::addScreen3D(Screen* screen, int priority)
{
    screenManager3D.setPrimary(screen, priority);
}

int Engine::getFps()
{
    return fps;
}

std::string Engine::getRevision()
{
    return revisionNumber;
}

int Engine::getScreenWidth()
{
    return screenWidth;
}

int Engine::getScreenHeight()
{
    return screenHeight;
}

int Engine::getScreenDepth()
{
    return screenDepth;
}

void Engine::enumScreenModes()
{
    screenModes.clear();

	SDL_Rect** modes;
	modes = SDL_ListModes(NULL, sdlFlags|SDL_FULLSCREEN); // Make sure the sdl flags include fullscreen for listing modes
	if(modes == (SDL_Rect **)0)
        return ; // No modes available
    if(modes == (SDL_Rect **)-1)
        return ; // All modes available
    // Otherwise, only some modes available. We'll store them for reference
    // (Note that SDL does not guarantee results to be sorted)
    //std::cout << "Available screen modes:" << std::endl;
    for(int i = 0; modes[i]; i++)
    {
        screenModes.push_back(ScreenMode(modes[i]->w, modes[i]->h));
        //std::cout << "\t" << modes[i]->w << " x " << modes[i]->h << std::endl;
    }

}

void Engine::log(std::string msg)
{
    eventLog.push_back(msg);
    std::cout << msg << std::endl;
}

void Engine::quit()
{
    isRunning = false;
    std::cout << "Engine quit signalled." << std::endl;
}
